﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using LitJson;
using System.Web;

namespace SynoManager.DownloadStation
{
    /* WireShark Filter text
     * ((ip.dst == 192.168.10.48) || (ip.src == 192.168.10.48)) && ((tcp.dstport == 5000) || (tcp.srcport == 5000))
     * (((ip.dst == 192.168.10.48) || (ip.src == 192.168.10.48)) && ((tcp.dstport == 5000) || (tcp.srcport == 5000))) && (http)
     */
    internal class DownloadRedirector
    {
        private readonly DownloadStationService service;
        private readonly string scriptLocation;
        private string sessionID;
        private string username;
        private readonly bool secure;

        internal DownloadRedirector(DownloadStationService service)
        {
            this.service = service;

            secure = service.Device.PreferSecure;
            
            if (secure)
                AcceptAllCertificatesPolicy.Install();

            scriptLocation = String.Format(
                "{0}://{1}:{2}/download/download_redirector.cgi", 
                (secure) ? "https" : "http",
                service.Device.Address, 
                (secure) ? service.Device.SecurePort : service.Device.Port);
        }

        #region Methods
        #region Login
        public LoginResult Login(string username, string password)
        {
            sessionID = null;
            LoginResult result = GetResult<LoginResult>("POST", "login", "username={0}&passwd={1}", username, password);
            if (result.login_success)
            {
                sessionID = result.id;
                this.username = username;
            }
            return result;
        }

#pragma warning disable 649 // Field ... is never assigned to, and will always have its default value
        internal class LoginResult : DownloadRedirectorResult
        {
            public bool login_success;
            public string id;
        }
#pragma warning restore 649
        #endregion

        #region GetAll
        public GetAllResult GetAll()
        {
            return GetResult<GetAllResult>("GET", "getall", "");
        }

#pragma warning disable 649 // Field ... is never assigned to, and will always have its default value
        internal class GetAllResult : DownloadRedirectorResult
        {
            public DownloadTaskInfo[] items;
            public int total;
        }

        internal class DownloadTaskInfo
        {
            public int id;
            public string username;
            public int pid;
            public string url;
            public string filename;
            public int status;
            public int createdTime;
            public int startedTime;
            public string totalSize;
            public string currentSize;
            public int currentRate;
            public string extraInfo;
            public int totalPeers;
            public string progress;
            public int connectedPeers;
            public int totalPieces;
            public int downloadedPieces;
            public int availablePieces;
            public int uploadRate;
            public long totalUpload;
            public int seedingRatio;
            public int seedingInterval;
        }
#pragma warning restore 649
        #endregion

        #region Stop/Resume/Delete
        private const string idListSeperator = ":";
        public DownloadRedirectorResult Stop(params int[] taskIds)
        {
            return ListTask("stop", taskIds);
        }

        public DownloadRedirectorResult Resume(params int[] taskIds)
        {
            return ListTask("resume", taskIds);
        }

        public DownloadRedirectorResult Delete(params int[] taskIds)
        {
            return ListTask("delete", taskIds);
        }

        private DownloadRedirectorResult ListTask(string action, int[] taskIds)
        {
            StringBuilder sb = new StringBuilder();
            foreach (int taskId in taskIds)
            {
                sb.Append(taskId);
                sb.Append(idListSeperator);
            }
            sb.Remove(sb.Length - idListSeperator.Length, idListSeperator.Length);
            return GetResult<DownloadRedirectorResult>("GET", action, "idList=" + sb);
        }
        #endregion

        #region Add URL / File
        public DownloadRedirectorResult AddUrl(string url)
        {
            DownloadRedirectorResult result = null;

            try
            {
                result = GetResult<DownloadRedirectorResult>("POST", "addurl", "url={0}", url);
            }
            catch (Exception)
            {
                try
                {
                    //something wrong. Wait a little and try it again
                    System.Threading.Thread.Sleep(100);
                    result = GetResult<DownloadRedirectorResult>("POST", "addurl", "url={0}", url);
                }
                catch (Exception)
                {
                    throw;
                }

            }
            return result;
        }

        public DownloadRedirectorResult AddFile(string path)
        {
            // TODO: refactor this method
            FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
            byte[] data = new byte[stream.Length];
            int offset = 0;
            int remaining = data.Length;
            while (remaining > 0)
            {
                int read = stream.Read(data, offset, remaining);
                if (read <= 0)
                    throw new EndOfStreamException
                        (String.Format("End of stream reached with {0} bytes left to read", remaining));
                remaining -= read;
                offset += read;
            }
            stream.Close();

            string boundary = "-----------------------" + DateTime.Now.Ticks.ToString("x");

            var request = (HttpWebRequest)WebRequest.Create(scriptLocation);
            request.Method = "POST";
            request.Accept = "*/*";
            request.ContentType = "multipart/form-data; boundary=" + boundary;
            request.SendChunked = false;
            

            // Build up the post message header
            StringBuilder sb = new StringBuilder();
            sb.Append("--");
            sb.Append(boundary);
            sb.Append("\r\n");
            sb.Append("Content-Disposition: form-data; ");
            sb.Append("name=\"id\"");
            sb.Append("\r\n");
            sb.Append("\r\n");
            sb.Append(sessionID);
            sb.Append("\r\n");
            sb.Append("--");
            sb.Append(boundary);
            sb.Append("\r\n");
            sb.Append("Content-Disposition: form-data; ");
            sb.Append("name=\"torrent\"; ");
            sb.Append("filename=\"" + Path.GetFileName(path) + "\"");
            sb.Append("\r\n");
            sb.Append("Content-Type: application/octet-stream");
            sb.Append("\r\n");
            sb.Append("\r\n");

            string postHeader = sb.ToString();
            byte[] postHeaderBytes = Encoding.ASCII.GetBytes(postHeader);

            // Build the trailing boundary string as a byte array
            // ensuring the boundary appears on a line by itself
            byte[] boundaryBytes =
                   Encoding.ASCII.GetBytes("\r\n--" + boundary + "--\r\n");

            long length = postHeaderBytes.Length + data.Length +
                                                   boundaryBytes.Length;
            request.ContentLength = length;
            Stream requestStream = request.GetRequestStream();

            // Write out our post header
            requestStream.Write(postHeaderBytes, 0, postHeaderBytes.Length);

            // Write out the file contents

            for (int i = 0; i < data.Length; i++)
                requestStream.WriteByte(data[i]);

            // Write out the trailing boundary
            requestStream.Write(boundaryBytes, 0, boundaryBytes.Length);

            var response = (HttpWebResponse) request.GetResponse();
            var responseStream = new StreamReader(response.GetResponseStream());
            var result = responseStream.ReadToEnd();
            DownloadRedirectorResult obj = JsonMapper.ToObject<DownloadRedirectorResult>(result);

            Debug.Assert(obj.success, "Action " + "add" + " on Download Redirector failed");

            return obj;
        }

        #endregion

        #region Clear
        public DownloadRedirectorResult Clear()
        {
            return GetResult<DownloadRedirectorResult>("POST", "clear", "");
        }

        #endregion

        #region Shares
        public RwShareGetResult RwShareGet()
        {
            return GetResult<RwShareGetResult>("GET", "rwshareget", "");
            
        }

#pragma warning disable 649 // Field ... is never assigned to, and will always have its default value
        public class RwShareGetResult : DownloadRedirectorResult
        {
            public string @default;
            public RwShareInfo[] items;
            public bool user_disabled;
        }

        public class RwShareInfo
        {
            public string name;
        }
#pragma warning restore 649

        public ShareGetResult ShareGet()
        {
            return GetResult<ShareGetResult>("GET", "shareget", "");

        }

        public DownloadRedirectorResult ShareSet(string share)
        {
            DownloadRedirectorResult result = GetResult<DownloadRedirectorResult>("POST", "shareset", "share={0}", share);
            Debug.Assert(result.success);
            return result;
        }

#pragma warning disable 649 // Field ... is never assigned to, and will always have its default value
        public class ShareGetResult : DownloadRedirectorResult
        {
            public string share_folder;
            public bool user_disabled;
        }
#pragma warning restore 649
        #endregion
        #region GetConfig
        public GetConfResult GetConf()
        {
            return GetResult<GetConfResult>("GET", "getconf", "");
        }

#pragma warning disable 649 // Field ... is never assigned to, and will always have its default value
        public class GetConfResult : DownloadRedirectorResult
        {
            public int dl_enable;
            public int dl_order;
            public string dl_time;
            public int seeding_int;
            public int seeding_ratio;
        }
#pragma warning restore 649
        #endregion
        #endregion

        public T GetResult<T>(string method, string action, string data, params string[] args) where T: DownloadRedirectorResult
        {
            for (int i = 0; i < args.Length; i++)
                args[i] = HttpUtility.UrlEncode(args[i]);

            List<string> arguments = new List<string>();
            data = String.Format(data, args);
            if (!String.IsNullOrEmpty(data))
                arguments.Add(data);

            if (sessionID != null)
                arguments.Add("id=" + sessionID);

            if (action != null)
                arguments.Add("action=" + action);

            if (username != null)
                arguments.Add("username=" + username);

            string result = GetResult(method, String.Join("&", arguments.ToArray()));

            T obj;
            try
            {
                obj = JsonMapper.ToObject<T>(result);
            }
            catch (JsonException ex)
            {
                throw new CommunicationException("Download Station returned data in an unexpected format", ex);
            }
            Debug.Assert(obj.success, "Action " + action + " on Download Redirector failed");

            return obj;
        }

        private string GetResult(string method, string data)
        {
            string url = scriptLocation;
            if (method == "GET")
                url += "?" + data;

            try
            {
                var request = (HttpWebRequest)HttpWebRequest.Create(url);
                request.Method = method;

                if (method == "POST")
                {
                    request.ContentType = "application/x-www-form-urlencoded";
                    var requestStream = new StreamWriter(request.GetRequestStream());
                    requestStream.Write(data);
                    requestStream.Flush();
                    requestStream.Close();
                }

                var response = (HttpWebResponse)request.GetResponse();
                var responseStream = new StreamReader(response.GetResponseStream());

                service.Device.IsConnected = true;

                return responseStream.ReadToEnd();
            }
            catch (WebException ex)
            {
                //TODO: Add fix for ex.Status == KeepAliveFailure

                service.Device.IsConnected = false;
                throw new CommunicationException("Could not communicate with Download Station at " + service.Device.Address, ex);
                //TODO: Switch to non-secure if a are connected secure now and try if that works
            }
            catch (UriFormatException ex)
            {
                throw new CommunicationException("Invalid location " + service.Device.Address, ex);
            }
        }

#pragma warning disable 649 // Field ... is never assigned to, and will always have its default value
        internal class DownloadRedirectorResult
        {
            public bool success;
        }
#pragma warning restore 649
    }
}
